/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.core.converter.converter;

import com.je.bpm.core.converter.constants.BpmnXMLConstants;
import com.je.bpm.core.converter.converter.alfresco.AlfrescoStartEventXMLConverter;
import com.je.bpm.core.converter.converter.alfresco.AlfrescoUserTaskXMLConverter;
import com.je.bpm.core.converter.converter.child.*;
import com.je.bpm.core.converter.converter.child.config.ProcessButtonConfigParser;
import com.je.bpm.core.converter.converter.child.multi.instance.MultiInstanceParser;
import com.je.bpm.core.converter.converter.export.*;
import com.je.bpm.core.converter.converter.parser.*;
import com.je.bpm.core.converter.converter.util.BpmnXMLUtil;
import com.je.bpm.core.converter.converter.util.InputStreamProvider;
import com.je.bpm.core.converter.exceptions.XMLException;
import com.je.bpm.core.model.*;
import com.je.bpm.core.model.artifact.Artifact;
import com.je.bpm.core.model.artifact.Association;
import com.je.bpm.core.model.artifact.TextAnnotation;
import com.je.bpm.core.model.event.BoundaryEvent;
import com.je.bpm.core.model.lane.Pool;
import com.je.bpm.core.model.process.Process;
import com.je.bpm.core.model.process.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.stream.*;
import javax.xml.transform.stax.StAXSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;

public class BpmnXMLConverter implements BpmnXMLConstants {

    protected static final Logger LOGGER = LoggerFactory.getLogger(BpmnXMLConverter.class);

    protected static final String BPMN_XSD = "com/je/bpm/parser/BPMN20.xsd";
    protected static final String DEFAULT_ENCODING = "UTF-8";

    protected static Map<String, BaseBpmnXMLConverter> convertersToBpmnMap = new HashMap<String, BaseBpmnXMLConverter>();
    protected static Map<Class<? extends BaseElement>, BaseBpmnXMLConverter> convertersToXMLMap = new HashMap<Class<? extends BaseElement>, BaseBpmnXMLConverter>();

    protected ClassLoader classloader;
    protected List<String> userTaskFormTypes;
    protected List<String> startEventFormTypes;

    protected BpmnEdgeParser bpmnEdgeParser = new BpmnEdgeParser();
    protected BpmnShapeParser bpmnShapeParser = new BpmnShapeParser();
    protected DefinitionsParser definitionsParser = new DefinitionsParser();
    protected DocumentationParser documentationParser = new DocumentationParser();
    protected ExtensionElementsParser extensionElementsParser = new ExtensionElementsParser();
    protected ImportParser importParser = new ImportParser();
    protected InterfaceParser interfaceParser = new InterfaceParser();
    protected ItemDefinitionParser itemDefinitionParser = new ItemDefinitionParser();
    protected IOSpecificationParser ioSpecificationParser = new IOSpecificationParser();
    protected DataStoreParser dataStoreParser = new DataStoreParser();
    protected LaneParser laneParser = new LaneParser();
    protected MessageParser messageParser = new MessageParser();
    protected MessageFlowParser messageFlowParser = new MessageFlowParser();
    protected MultiInstanceParser multiInstanceParser = new MultiInstanceParser();
    protected ParticipantParser participantParser = new ParticipantParser();
    protected PotentialStarterParser potentialStarterParser = new PotentialStarterParser();
    protected ProcessParser processParser = new ProcessParser();
    protected ResourceParser resourceParser = new ResourceParser();
    protected SignalParser signalParser = new SignalParser();
    protected SubProcessParser subProcessParser = new SubProcessParser();

    static {
        // events
        addConverter(new EndEventXMLConverter());
        addConverter(new StartEventXMLConverter());

        // tasks
        addConverter(new BusinessRuleTaskXMLConverter());
        addConverter(new ManualTaskXMLConverter());
        addConverter(new ReceiveTaskXMLConverter());
        addConverter(new ScriptTaskXMLConverter());
        addConverter(new ServiceTaskXMLConverter());
        addConverter(new SendTaskXMLConverter());
        addConverter(new UserTaskXMLConverter());

        addConverter(new TaskXMLConverter());
        addConverter(new CallActivityXMLConverter());

        // gateways
        addConverter(new EventGatewayXMLConverter());
        addConverter(new ExclusiveGatewayXMLConverter());
        addConverter(new InclusiveGatewayXMLConverter());
        addConverter(new ParallelGatewayXMLConverter());
        addConverter(new ComplexGatewayXMLConverter());

        // connectors
        addConverter(new SequenceFlowXMLConverter());

        // catch, throw and boundary event
        addConverter(new CatchEventXMLConverter());
        addConverter(new ThrowEventXMLConverter());
        addConverter(new BoundaryEventXMLConverter());

        // artifacts
        addConverter(new TextAnnotationXMLConverter());
        addConverter(new AssociationXMLConverter());

        // data store reference
        addConverter(new DataStoreReferenceXMLConverter());

        // data objects
        addConverter(new ValuedDataObjectXMLConverter(), StringDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), BooleanDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), IntegerDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), LongDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), DoubleDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), DateDataObject.class);

        // Alfresco types
        addConverter(new AlfrescoStartEventXMLConverter());
        addConverter(new AlfrescoUserTaskXMLConverter());

        //新增，自定义节点解析
        addConverter(new KaiteUserTaskXmlConverter());
        addConverter(new KaiteMultiUserTaskXMLConverter());
        addConverter(new KaiteLoopUserTaskXMLConverter());
        addConverter(new KaiteFixedUserTaskXMLConverter());
        addConverter(new KaiteDecideUserTaskXMLConverter());
        addConverter(new KaiteCandidateUserTaskXmlConverter());
        addConverter(new KaiteCounterSignUserTaskXMLConverter());
        addConverter(new KaiteRandomUserTaskXMLConverter());

        //流程基础属性
        addConverter(new ProcessBasicConfigParser());
        addConverter(new ProcessExtendedConfigParser());
        addConverter(new ProcessEventConfigParser());
        addConverter(new ProcessMessageConfigParser());
        addConverter(new ProcessStartConfigParser());
        addConverter(new ProcessButtonConfigParser());

        //
//        ProcessBasicConfigParser processBasicConfigParser = new ProcessBasicConfigParser();
//        //流程拓展属性
//        ProcessExtendedConfigParser processExtendedConfigParser = new ProcessExtendedConfigParser();
//        //流程自定义事件
//        ProcessEventConfigParser processEventConfigParser = new ProcessEventConfigParser();
//        //流程自定义消息
//        ProcessMessageConfigParser processMessageConfigParser = new ProcessMessageConfigParser();
//        //流程启动配置
//        ProcessStartConfigParser processStartConfigParser = new ProcessStartConfigParser();
//        //流程按钮
//        ProcessButtonConfigParser processButtonConfigParser = new ProcessButtonConfigParser();
//
//        childParserMap.put(processBasicConfigParser.getElementName(), processBasicConfigParser);
//        childParserMap.put(processExtendedConfigParser.getElementName(), processExtendedConfigParser);
//        childParserMap.put(processEventConfigParser.getElementName(), processEventConfigParser);
//        childParserMap.put(processMessageConfigParser.getElementName(), processMessageConfigParser);
//        childParserMap.put(processStartConfigParser.getElementName(), processStartConfigParser);
//        childParserMap.put(processButtonConfigParser.getElementName(), processButtonConfigParser);

    }

    public static void addConverter(BaseBpmnXMLConverter converter) {
        addConverter(converter, converter.getBpmnElementType());
    }

    public static void addConverter(BaseBpmnXMLConverter converter, Class<? extends BaseElement> elementType) {
        convertersToBpmnMap.put(converter.getXMLElementName(), converter);
        convertersToXMLMap.put(elementType, converter);
    }

    public void setClassloader(ClassLoader classloader) {
        this.classloader = classloader;
    }

    public void setUserTaskFormTypes(List<String> userTaskFormTypes) {
        this.userTaskFormTypes = userTaskFormTypes;
    }

    public void setStartEventFormTypes(List<String> startEventFormTypes) {
        this.startEventFormTypes = startEventFormTypes;
    }

    public void validateModel(InputStreamProvider inputStreamProvider) throws Exception {
        Schema schema = createSchema();
        Validator validator = schema.newValidator();
        validator.validate(new StreamSource(inputStreamProvider.getInputStream()));
    }

    public void validateModel(XMLStreamReader xmlStreamReader) throws Exception {
        Schema schema = createSchema();
        Validator validator = schema.newValidator();
        validator.validate(new StAXSource(xmlStreamReader));
    }

    protected Schema createSchema() throws SAXException {
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema schema = null;
        if (classloader != null) {
            schema = createSchema(factory, classloader.getResource(BPMN_XSD));
        }
        if (schema == null) {
            schema = createSchema(factory, BpmnXMLConverter.class.getClassLoader().getResource(BPMN_XSD));
        }
        if (schema == null) {
            throw new XMLException("BPMN XSD could not be found");
        }
        return schema;
    }

    protected Schema createSchema(SchemaFactory factory, URL schemaUrl) throws SAXException {
        try {
            return factory.newSchema(new StreamSource(schemaUrl.toURI().toASCIIString()));
        } catch (URISyntaxException e) {
            throw new XMLException("BPMN XSD could not be found", e);
        }
    }

    public BpmnModel convertToBpmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema, boolean enableSafeBpmnXml) {
        return convertToBpmnModel(inputStreamProvider, validateSchema, enableSafeBpmnXml, DEFAULT_ENCODING);
    }

    public BpmnModel convertToBpmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema, boolean enableSafeBpmnXml, String encoding) {
        XMLInputFactory xif = XMLInputFactory.newInstance();
        if (xif.isPropertySupported(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES)) {
            xif.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
        }
        if (xif.isPropertySupported(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES)) {
            xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        }
        if (xif.isPropertySupported(XMLInputFactory.SUPPORT_DTD)) {
            xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        }

        InputStreamReader in = null;
        try {
            in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding);
            XMLStreamReader xtr = xif.createXMLStreamReader(in);
            try {
                if (validateSchema) {
                    if (!enableSafeBpmnXml) {
                        validateModel(inputStreamProvider);
                    } else {
                        validateModel(xtr);
                    }
                    // The input stream is closed after schema validation
                    in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding);
                    xtr = xif.createXMLStreamReader(in);
                }
            } catch (Exception e) {
                throw new XMLException(e.getMessage(), e);
            }
            // XML conversion
            return convertToBpmnModel(xtr);
        } catch (UnsupportedEncodingException e) {
            throw new XMLException("The bpmn 2.0 xml is not UTF8 encoded", e);
        } catch (XMLStreamException e) {
            throw new XMLException("Error while reading the BPMN 2.0 XML", e);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    LOGGER.debug("Problem closing BPMN input stream", e);
                }
            }
        }
    }

    public BpmnModel convertToBpmnModel(XMLStreamReader xtr) {
        BpmnModel model = new BpmnModel();
        model.setStartEventFormTypes(startEventFormTypes);
        model.setUserTaskFormTypes(userTaskFormTypes);
        try {
            Process activeProcess = null;
            List<SubProcess> activeSubProcessList = new ArrayList<SubProcess>();
            while (xtr.hasNext()) {
                try {
                    xtr.next();
                } catch (Exception e) {
                    LOGGER.debug("Error reading XML document", e);
                    throw new XMLException("Error reading XML", e);
                }

                //子流程
                if (xtr.isEndElement() && (ELEMENT_SUBPROCESS.equals(xtr.getLocalName()) || ELEMENT_TRANSACTION.equals(xtr.getLocalName()) || ELEMENT_ADHOC_SUBPROCESS.equals(xtr.getLocalName()))) {
                    activeSubProcessList.remove(activeSubProcessList.size() - 1);
                }

                //不是开始节点，则执行下一个
                if (!xtr.isStartElement()) {
                    continue;
                }
                //流程定义，比如 targetNamespace http://activiti.org/test
                if (ELEMENT_DEFINITIONS.equals(xtr.getLocalName())) {
                    definitionsParser.parse(xtr, model);
                } else if (ELEMENT_RESOURCE.equals(xtr.getLocalName())) {
                    resourceParser.parse(xtr, model);
                } else if (ELEMENT_SIGNAL.equals(xtr.getLocalName())) {
                    signalParser.parse(xtr, model);
                } else if (ELEMENT_MESSAGE.equals(xtr.getLocalName())) {
                    messageParser.parse(xtr, model);
                } else if (ELEMENT_ERROR.equals(xtr.getLocalName())) {
                    if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_ID))) {
                        model.addError(xtr.getAttributeValue(null, ATTRIBUTE_ID),
                                xtr.getAttributeValue(null, ATTRIBUTE_NAME),
                                xtr.getAttributeValue(null, ATTRIBUTE_ERROR_CODE));
                    }
                } else if (ELEMENT_IMPORT.equals(xtr.getLocalName())) {
                    importParser.parse(xtr, model);
                } else if (ELEMENT_ITEM_DEFINITION.equals(xtr.getLocalName())) {
                    itemDefinitionParser.parse(xtr, model);
                } else if (ELEMENT_DATA_STORE.equals(xtr.getLocalName())) {
                    dataStoreParser.parse(xtr, model);
                } else if (ELEMENT_INTERFACE.equals(xtr.getLocalName())) {
                    interfaceParser.parse(xtr, model);
                } else if (ELEMENT_IOSPECIFICATION.equals(xtr.getLocalName())) {
                    ioSpecificationParser.parseChildElement(xtr, activeProcess, model);
                } else if (ELEMENT_PARTICIPANT.equals(xtr.getLocalName())) {
                    participantParser.parse(xtr, model);
                } else if (ELEMENT_MESSAGE_FLOW.equals(xtr.getLocalName())) {
                    messageFlowParser.parse(xtr, model);
                } else if (ELEMENT_PROCESS.equals(xtr.getLocalName())) {
                    //解析流程属性
                    Process process = processParser.parse(xtr, model);
                    if (process != null) {
                        activeProcess = process;
                    }
                } else if (ELEMENT_POTENTIAL_STARTER.equals(xtr.getLocalName())) {
                    potentialStarterParser.parse(xtr, activeProcess);
                } else if (ELEMENT_LANE.equals(xtr.getLocalName())) {
                    laneParser.parse(xtr, activeProcess, model);
                } else if (ELEMENT_DOCUMENTATION.equals(xtr.getLocalName())) {
                    BaseElement parentElement = null;
                    if (!activeSubProcessList.isEmpty()) {
                        parentElement = activeSubProcessList.get(activeSubProcessList.size() - 1);
                    } else if (activeProcess != null) {
                        parentElement = activeProcess;
                    }
                    documentationParser.parseChildElement(xtr, parentElement, model);
                } else if (activeProcess == null && ELEMENT_TEXT_ANNOTATION.equals(xtr.getLocalName())) {
                    String elementId = xtr.getAttributeValue(null, ATTRIBUTE_ID);
                    TextAnnotation textAnnotation = (TextAnnotation) new TextAnnotationXMLConverter().convertXMLToElement(xtr, model);
                    textAnnotation.setId(elementId);
                    model.getGlobalArtifacts().add(textAnnotation);
                } else if (activeProcess == null && ELEMENT_ASSOCIATION.equals(xtr.getLocalName())) {
                    String elementId = xtr.getAttributeValue(null, ATTRIBUTE_ID);
                    Association association = (Association) new AssociationXMLConverter().convertXMLToElement(xtr, model);
                    association.setId(elementId);
                    model.getGlobalArtifacts().add(association);
                } else if (ELEMENT_EXTENSIONS.equals(xtr.getLocalName())) {
                    extensionElementsParser.parse(xtr, activeSubProcessList, activeProcess, model);
                } else if (ELEMENT_SUBPROCESS.equals(xtr.getLocalName()) || ELEMENT_TRANSACTION.equals(xtr.getLocalName()) || ELEMENT_ADHOC_SUBPROCESS.equals(xtr.getLocalName())) {
                    subProcessParser.parse(xtr, activeSubProcessList, activeProcess);
                } else if (ELEMENT_COMPLETION_CONDITION.equals(xtr.getLocalName())) {
                    if (!activeSubProcessList.isEmpty()) {
                        SubProcess subProcess = activeSubProcessList.get(activeSubProcessList.size() - 1);
                        if (subProcess instanceof AdhocSubProcess) {
                            AdhocSubProcess adhocSubProcess = (AdhocSubProcess) subProcess;
                            adhocSubProcess.setCompletionCondition(xtr.getElementText());
                        }
                    }
                } else if (ELEMENT_DI_SHAPE.equals(xtr.getLocalName())) {
                    bpmnShapeParser.parse(xtr, model);
                } else if (ELEMENT_DI_EDGE.equals(xtr.getLocalName())) {
                    bpmnEdgeParser.parse(xtr, model);
                } else {
                    if (!activeSubProcessList.isEmpty() && ELEMENT_MULTIINSTANCE.equalsIgnoreCase(xtr.getLocalName())) {
                        multiInstanceParser.parseChildElement(xtr, activeSubProcessList.get(activeSubProcessList.size() - 1), model);
                    } else if (convertersToBpmnMap.containsKey(xtr.getLocalName())) {
                        if (activeProcess != null) {
                            BaseBpmnXMLConverter converter = convertersToBpmnMap.get(xtr.getLocalName());
                            converter.convertToBpmnModel(xtr, model, activeProcess, activeSubProcessList);
                        }
                    }
                }
            }

            for (Process process : model.getProcesses()) {
                for (Pool pool : model.getPools()) {
                    if (process.getId().equals(pool.getProcessRef())) {
                        pool.setExecutable(process.isExecutable());
                    }
                }
                processFlowElements(process.getFlowElements(), process);
            }

        } catch (XMLException e) {
            throw e;
        } catch (Exception e) {
            LOGGER.error("Error processing BPMN document", e);
            throw new XMLException("Error processing BPMN document", e);
        }
        return model;
    }

    protected void processFlowElements(Collection<FlowElement> flowElementList, BaseElement parentScope) {
        for (FlowElement flowElement : flowElementList) {
            if (flowElement instanceof SequenceFlow) {
                SequenceFlow sequenceFlow = (SequenceFlow) flowElement;
                FlowNode sourceNode = getFlowNodeFromScope(sequenceFlow.getSourceRef(), parentScope);
                if (sourceNode != null) {
                    sourceNode.getOutgoingFlows().add(sequenceFlow);
                    sequenceFlow.setSourceFlowElement(sourceNode);
                }
                FlowNode targetNode = getFlowNodeFromScope(sequenceFlow.getTargetRef(), parentScope);
                if (targetNode != null) {
                    targetNode.getIncomingFlows().add(sequenceFlow);
                    sequenceFlow.setTargetFlowElement(targetNode);
                }
            } else if (flowElement instanceof BoundaryEvent) {
                BoundaryEvent boundaryEvent = (BoundaryEvent) flowElement;
                FlowElement attachedToElement = getFlowNodeFromScope(boundaryEvent.getAttachedToRefId(), parentScope);
                if (attachedToElement instanceof Activity) {
                    Activity attachedActivity = (Activity) attachedToElement;
                    boundaryEvent.setAttachedToRef(attachedActivity);
                    attachedActivity.getBoundaryEvents().add(boundaryEvent);
                }
            } else if (flowElement instanceof SubProcess) {
                SubProcess subProcess = (SubProcess) flowElement;
                processFlowElements(subProcess.getFlowElements(), subProcess);
            }
        }
    }

    protected FlowNode getFlowNodeFromScope(String elementId, BaseElement scope) {
        FlowNode flowNode = null;
        if (StringUtils.isNotEmpty(elementId)) {
            if (scope instanceof Process) {
                flowNode = (FlowNode) ((Process) scope).getFlowElement(elementId);
            } else if (scope instanceof SubProcess) {
                flowNode = (FlowNode) ((SubProcess) scope).getFlowElement(elementId);
            }
        }
        return flowNode;
    }

    public byte[] convertToXML(BpmnModel model) {
        return convertToXML(model, DEFAULT_ENCODING);
    }

    public byte[] convertToXML(BpmnModel model, String encoding) {
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            XMLOutputFactory xof = XMLOutputFactory.newInstance();
            OutputStreamWriter out = new OutputStreamWriter(outputStream, encoding);
            XMLStreamWriter writer = xof.createXMLStreamWriter(out);
            XMLStreamWriter xtw = new IndentingXMLStreamWriter(writer);
            DefinitionsRootExport.writeRootElement(model, xtw, encoding);
            CollaborationExport.writePools(model, xtw);
            DataStoreExport.writeDataStores(model, xtw);
            SignalAndMessageDefinitionExport.writeSignalsAndMessages(model, xtw);

            for (Process process : model.getProcesses()) {
                if (process.getFlowElements().isEmpty() && process.getLanes().isEmpty()) {
                    // empty process, ignore it
                    continue;
                }
                ProcessExport.writeProcess(process, xtw);
                for (FlowElement flowElement : process.getFlowElements()) {
                    createXML(flowElement, model, xtw);
                }
                for (Artifact artifact : process.getArtifacts()) {
                    createXML(artifact, model, xtw);
                }
                // end process element
                xtw.writeEndElement();
            }
            ErrorExport.writeError(model, xtw);
            BPMNDIExport.writeBPMNDI(model, xtw);

            // end definitions root element
            xtw.writeEndElement();
            xtw.writeEndDocument();
            xtw.flush();
            outputStream.close();
            xtw.close();
            return outputStream.toByteArray();
        } catch (Exception e) {
            LOGGER.error("Error writing BPMN XML", e);
            throw new XMLException("Error writing BPMN XML", e);
        }
    }

    protected void createXML(FlowElement flowElement, BpmnModel model, XMLStreamWriter xtw) throws Exception {
        if (flowElement instanceof SubProcess) {
            SubProcess subProcess = (SubProcess) flowElement;
            if (flowElement instanceof Transaction) {
                xtw.writeStartElement(ELEMENT_TRANSACTION);
            } else if (flowElement instanceof AdhocSubProcess) {
                xtw.writeStartElement(ELEMENT_ADHOC_SUBPROCESS);
            } else {
                xtw.writeStartElement(BPMN2_PREFIX, ELEMENT_SUBPROCESS, BPMN2_NAMESPACE);
            }
            xtw.writeAttribute(ATTRIBUTE_ID, subProcess.getId());
            if (StringUtils.isNotEmpty(subProcess.getName())) {
                xtw.writeAttribute(ATTRIBUTE_NAME, subProcess.getName());
            } else {
                xtw.writeAttribute(ATTRIBUTE_NAME, "subProcess");
            }
            if (subProcess instanceof EventSubProcess) {
                xtw.writeAttribute(ATTRIBUTE_TRIGGERED_BY, ATTRIBUTE_VALUE_TRUE);
            } else if (!(subProcess instanceof Transaction)) {
                if (subProcess.isAsynchronous()) {
                    BpmnXMLUtil.writeQualifiedAttribute(ATTRIBUTE_ACTIVITY_ASYNCHRONOUS, ATTRIBUTE_VALUE_TRUE, xtw);
                    if (subProcess.isNotExclusive()) {
                        BpmnXMLUtil.writeQualifiedAttribute(ATTRIBUTE_ACTIVITY_EXCLUSIVE, ATTRIBUTE_VALUE_FALSE, xtw);
                    }
                }
            } else if (subProcess instanceof AdhocSubProcess) {
                AdhocSubProcess adhocSubProcess = (AdhocSubProcess) subProcess;
                BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_CANCEL_REMAINING_INSTANCES, String.valueOf(adhocSubProcess.isCancelRemainingInstances()), xtw);
                if (StringUtils.isNotEmpty(adhocSubProcess.getOrdering())) {
                    BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_ORDERING, adhocSubProcess.getOrdering(), xtw);
                }
            }

            if (StringUtils.isNotEmpty(subProcess.getDocumentation())) {
                xtw.writeStartElement(BPMN2_PREFIX, ELEMENT_DOCUMENTATION, BPMN2_NAMESPACE);
                xtw.writeCharacters(subProcess.getDocumentation());
                xtw.writeEndElement();
            }

            boolean didWriteExtensionStartElement = ActivitiListenerExport.writeListeners(subProcess, false, xtw);
            didWriteExtensionStartElement = BpmnXMLUtil.writeExtensionElements(subProcess, didWriteExtensionStartElement, model.getNamespaces(), xtw);
            if (didWriteExtensionStartElement) {
                // closing extensions element
                xtw.writeEndElement();
            }

            MultiInstanceExport.writeMultiInstance(subProcess, xtw);

            if (subProcess instanceof AdhocSubProcess) {
                AdhocSubProcess adhocSubProcess = (AdhocSubProcess) subProcess;
                if (StringUtils.isNotEmpty(adhocSubProcess.getCompletionCondition())) {
                    xtw.writeStartElement(ELEMENT_COMPLETION_CONDITION);
                    xtw.writeCData(adhocSubProcess.getCompletionCondition());
                    xtw.writeEndElement();
                }
            }

            for (FlowElement subElement : subProcess.getFlowElements()) {
                createXML(subElement, model, xtw);
            }

            for (Artifact artifact : subProcess.getArtifacts()) {
                createXML(artifact, model, xtw);
            }

            xtw.writeEndElement();

        } else {
            BaseBpmnXMLConverter converter = convertersToXMLMap.get(flowElement.getClass());
            if (converter == null) {
                throw new XMLException("No converter for " + flowElement.getClass() + " found");
            }
            converter.convertToXML(xtw, flowElement, model);
        }
    }

    protected void createXML(Artifact artifact, BpmnModel model, XMLStreamWriter xtw) throws Exception {
        BaseBpmnXMLConverter converter = convertersToXMLMap.get(artifact.getClass());
        if (converter == null) {
            throw new XMLException("No converter for " + artifact.getClass() + " found");
        }
        converter.convertToXML(xtw, artifact, model);
    }
}
